package log

import (
	"context"
	"fmt"
	. "gitee.com/kristas/booting-go/framework/common/util/console"
	"gitee.com/kristas/booting-go/framework/common/util/lang"
	"gitee.com/kristas/booting-go/framework/core/statement/types"
	"path"
	"runtime"
	"strconv"
	"strings"
	"sync"
	"time"
)

const (
	maximumCallerDepth int = 6
	DefaultDepth       int = 4
)

type logger struct {
	types.Component    `name:"logger"`
	Ctx                context.Context
	Logger             LoggerAdapter
	Conf               *Configure
	level              Level
	depth              int
	callerInitOnce     sync.Once
	minimumCallerDepth int
	loggerPackage      string
	filters            map[string][]string
}

func New(loggerAdapter LoggerAdapter, level Level, conf *Configure, depth int) (Logger, error) {
	l := &logger{
		Logger:  loggerAdapter,
		Conf:    conf,
		depth:   depth,
		filters: make(map[string][]string),
		level:   level,
	}
	for i := range l.Conf.Loggers {
		loggers := l.Conf.Loggers[i]
		level := strings.ToLower(loggers.Level)
		l.filters[level] = append(l.filters[level], loggers.Name)
	}
	return l, nil
}

func (l *logger) GetName() string {
	return l.Logger.Bean()
}

func (l *logger) Context(ctx context.Context) Logger {
	l.Ctx = ctx
	return l
}

func (l *logger) newMeta(level Level) *Meta {
	return &Meta{
		Ctx:       l.Ctx,
		Frame:     l.getCaller(),
		Level:     level,
		Timestamp: time.Now(),
	}
}

func (l *logger) Trace(format string, a ...interface{}) {
	if l.level > Trace {
		return
	}
	meta := l.newMeta(Trace)
	if l.doFilter(Trace, meta) {
		msg := l.build(meta, format, a...)
		l.Logger.Trace(msg, meta)
	}
}

func (l *logger) Debug(format string, a ...interface{}) {
	if l.level > Debug {
		return
	}
	meta := l.newMeta(Debug)
	if l.doFilter(Debug, meta) {
		msg := l.build(meta, format, a...)
		l.Logger.Debug(msg, meta)
	}
}

func (l *logger) Info(format string, a ...interface{}) {
	if l.level > Info {
		return
	}
	meta := l.newMeta(Info)
	if l.doFilter(Info, meta) {
		msg := l.build(meta, format, a...)
		l.Logger.Info(msg, meta)
	}
}

func (l *logger) Warn(format string, a ...interface{}) {
	if l.level > Warn {
		return
	}
	meta := l.newMeta(Warn)
	if l.doFilter(Warn, meta) {
		msg := l.build(meta, format, a...)
		l.Logger.Warn(msg, meta)
	}
}

func (l *logger) Error(format string, a ...interface{}) {
	if l.level > Error {
		return
	}
	meta := l.newMeta(Error)
	if l.doFilter(Error, meta) {
		msg := l.build(meta, format, a...)
		l.Logger.Error(msg, meta)
	}
}

func (l *logger) Panic(format string, a ...interface{}) {
	if l.level > Panic {
		return
	}
	meta := l.newMeta(Panic)
	if l.doFilter(Panic, meta) {
		msg := l.build(meta, format, a...)
		l.Logger.Panic(msg, meta)
	}
}

func (l *logger) Fatal(format string, a ...interface{}) {
	if l.level > Fatal {
		return
	}
	meta := l.newMeta(Fatal)
	if l.doFilter(Fatal, meta) {
		msg := l.build(meta, format, a...)
		l.Logger.Fatal(msg, meta)
	}
}

func (l *logger) doFilter(level Level, meta *Meta) bool {
	for _, s := range l.filters[level.String()] {
		if strings.HasPrefix(meta.Frame.Function, s) {
			return false
		}
	}
	return true
}

type parser func(spec *Spec, meta *Meta) string

func (l *logger) build(meta *Meta, format string, a ...interface{}) string {
	spec := l.Conf.Style.Spec
	s := lang.NewEmptyString()
	for _, tmp := range l.Conf.Style.Template {
		s = s.Concat(l.styleParser(tmp)(spec[tmp], meta))
	}
	message := fmt.Sprintf(format, a...)
	return s.Format(message).String()
}

func (l *logger) styleParser(template string) parser {
	switch template {
	case timestamp:
		return l.parseTimestamp
	case level:
		return l.parseLevel
	case function:
		return l.parseFunction
	case location:
		return l.parseLocation
	case separator:
		return l.parseSeparator
	case message:
		return l.parseMessage
	default:
		return func(spec *Spec, meta *Meta) string {
			return ""
		}
	}
}

func (l *logger) parseTimestamp(spec *Spec, meta *Meta) string {
	return Draw(spec.Color, meta.Timestamp.Format(spec.Layout))
}

func (l *logger) parseLevel(spec *Spec, meta *Meta) string {
	switch meta.Level {
	case Trace:
		return Drawf(BlueBold, spec.Layout, Trace.String())
	case Debug:
		return Drawf(CyanBold, spec.Layout, Debug.String())
	case Info:
		return Drawf(GreenBold, spec.Layout, Info.String())
	case Warn:
		return Drawf(YellowBold, spec.Layout, Warn.String())
	case Error:
		return Drawf(RedBold, spec.Layout, Error.String())
	case Fatal:
		return Drawf(RedBold, spec.Layout, Fatal.String())
	case Panic:
		return Drawf(RedBold, spec.Layout, Panic.String())
	}
	return ""
}

func (l *logger) parseFunction(spec *Spec, meta *Meta) string {
	fc := l.packageParser(meta.Frame.Function)
	return Draw(spec.Color, lang.NewString(spec.Layout).Format(fc).String())
}

func (l *logger) parseLocation(spec *Spec, meta *Meta) string {
	dir, file := path.Split(meta.Frame.File)
	line := strconv.Itoa(meta.Frame.Line)
	return Draw(spec.Color, lang.NewString(spec.Layout).
		ReplaceAll("dir", dir).
		ReplaceAll("file", file).
		ReplaceAll("line", line).String())
}

func (l *logger) parseSeparator(spec *Spec, meta *Meta) string {
	return Draw(spec.Color, spec.Layout)
}

func (l *logger) parseMessage(spec *Spec, meta *Meta) string {
	return Draw(spec.Color, spec.Layout)
}

func (l *logger) packageParser(pkg string) string {
	split := lang.NewString(pkg).ReplaceAll("/", ".").Split(".")
	pkgArr := split.ToSimple()
	var tmp []string
	skipIndex := l.Conf.SkipIndex
	for i, s := range pkgArr {
		if i < skipIndex {
			tmp = append(tmp, s[:1])
		} else {
			tmp = append(tmp, s)
		}
	}
	return strings.Join(tmp, ".")
}

func (l *logger) getCaller() *runtime.Frame {
	l.callerInitOnce.Do(func() {
		pcs := make([]uintptr, maximumCallerDepth)
		_ = runtime.Callers(0, pcs)

		for i := 0; i < maximumCallerDepth; i++ {
			funcName := runtime.FuncForPC(pcs[i]).Name()
			if strings.Contains(funcName, "getCaller") {
				l.loggerPackage = getPackageName(funcName)
				break
			}
		}
		l.minimumCallerDepth = l.depth
	})
	pcs := make([]uintptr, maximumCallerDepth)
	depth := runtime.Callers(l.minimumCallerDepth, pcs)
	frames := runtime.CallersFrames(pcs[:depth])

	for f, again := frames.Next(); again; f, again = frames.Next() {
		pkg := getPackageName(f.Function)
		if pkg != l.loggerPackage {
			return &f //nolint:scopelint
		}
	}
	return nil
}

func getPackageName(f string) string {
	for {
		lastPeriod := strings.LastIndex(f, ".")
		lastSlash := strings.LastIndex(f, "/")
		if lastPeriod > lastSlash {
			f = f[:lastPeriod]
		} else {
			break
		}
	}
	return f
}
